package com.googlecode.jawb;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Hashtable;

import com.googlecode.jawb.clues.SetArray;
import com.googlecode.jawb.clues.SetCondBigger;
import com.googlecode.jawb.clues.SetCondEqual;
import com.googlecode.jawb.clues.SetCondLesser;
import com.googlecode.jawb.clues.SetCondNotEqual;
import com.googlecode.jawb.clues.SetCondition;
import com.googlecode.jawb.clues.SetIgnored;
import com.googlecode.jawb.clues.SetInjection;
import com.googlecode.jawb.clues.SetItemPostVisitListener;
import com.googlecode.jawb.clues.SetPostVisitListener;
import com.googlecode.jawb.clues.SetPreVisitListener;
import com.googlecode.jawb.clues.SetType;
import com.googlecode.jawb.clues.SetType.DefaultClass;
import com.googlecode.jawb.clues.types.DynamicLengthType;
import com.googlecode.jawb.clues.types.StaticLengthType;

/**
 * Determines how to work with certain field based on attached annotations.
 */
public class AnnotationParser {
	// Input values
	private Object                          parsedObject                  = null;
	private Class<?>                        parsedClass                   = null;
	private Field                           parsedField                   = null;

	// Self-obtained values. Contains instances of annotations attached to
	// a current field (or null if not present).
	private SetIgnored                      paramSetIgnored               = null;
	private SetType                         paramSetType                  = null;
	private SetArray                        paramSetArray                 = null;
	private SetCondition                    paramSetCondition             = null;
	private SetInjection                    paramSetInjection             = null;
	private SetPreVisitListener             paramSetPreVisitListener      = null;
	private SetPostVisitListener            paramSetPostVisitListener     = null;
	private SetItemPostVisitListener		paramSetItemPostVisitListener = null;
	
	/**
	 * Contains all simple conditional annotations attached to a current field.
	 * Key contains translated type of a condition (from enumeration) and value
	 * contains reference to an instance of given annotation.
	 */
	private Hashtable<Condition, Object>    conditions                   = new Hashtable<>();
	
	/**
	 * Loads conditions and evaluates them. 
	 */
	protected enum Condition {
		EQUAL(SetCondEqual.class) {
			@Override
			protected boolean compare(long a, long b) {
				return a == b;
			}
		},
		NOT_EQUAL(SetCondNotEqual.class) {
			@Override
			protected boolean compare(long a, long b) {
				return a != b;
			}
		},
		BIGGER(SetCondBigger.class) {
			@Override
			protected boolean compare(long a, long b) {
				return a > b;
			}
		},
		LESSER(SetCondLesser.class) {
			@Override
			protected boolean compare(long a, long b) {
				return a < b;
			}
		};

		/**
		 * Class of an annotation which represents specific condition. It's used
		 * for loading of all conditions attached to a given field.
		 */
		private Class<? extends Annotation> annotationType = null;

		/**
		 * @param annotationType
		 * 	See {@link #annotationType}
		 */
		private Condition(Class<? extends Annotation> annotationType) {
			this.annotationType = annotationType;
		}

		/**
		 * @return
		 * 	See {@link #annotationType}
		 */
		private Class<? extends Annotation> getAnnotationType() {
			return this.annotationType;
		}

		/**
		 * <p>Compares two values using appropriate operator and returns result of
		 * that logical expression.</p>
		 * 
		 * <p>Given the values are evaluated using operator ~ then expression is
		 * always evaluated with values in the same order as following:</p>
		 * 
		 * <pre>
		 *   A ~ B
		 * </pre>
		 * 
		 * @param a
		 *            Runtime value (obtained from a required field).
		 * @param b
		 *            Constant value (to be compared with).
		 */
		protected abstract boolean compare(long a, long b);
		
		/**
		 * Evaluates a condition.
		 * 
		 * @param conditions
		 *            See {@link AnnotationParser#conditions}
		 * @param parsedObject
		 *            See {@link AnnotationParser#parsedObject}
		 * @param parsedField
		 *            See {@link AnnotationParser#parsedField}
		 * @return True if condition is matched.
		 */
		public boolean evaluate(Hashtable<Condition, Object> conditions, Object parsedObject, Field parsedField) {
			try {
				Object  annotation      = conditions.get(this);
				String  fieldName       = annotationGetFieldName(annotation);
				long    constantValue   = annotationGetValue(annotation);
				long    runtimeValue    = getValueOfField(parsedObject, fieldName);
				
				return this.compare(runtimeValue, constantValue);
			} catch (Exception e) {
				return false;
			}
		}
		
		/**
		 * Calls non-parametric method using reflection.
		 * 
		 * @param annotation
		 *            Instance of an annotation.
		 * @param methodName
		 *            Name of a method to be called.
		 * @return Value returned by invoked method.
		 * @throws Exception
		 *             Requested method couldn't be called. It suggests an
		 *             internal error and shouldn't happen in flawless code.
		 */
		private Object annotationCall(Object annotation, String methodName) throws Exception {
			return annotation.getClass().getDeclaredMethod(methodName).invoke(annotation);
		}
		
		/**
		 * @param annotation
		 *            Instance of an annotation.
		 * @return Name of a field requested by condition.
		 * @throws Exception
		 *             See {@link #annotationCall(Object, String)}
		 */
		private String annotationGetFieldName(Object annotation) throws Exception {
			return (String) this.annotationCall(annotation, "fieldName");
		}
		
		/**
		 * @param annotation
		 *            Instance of an annotation.
		 * @return Constant value defined in condition.
		 * @throws Exception
		 *             See {@link #annotationCall(Object, String)}
		 */
		private long annotationGetValue(Object annotation) throws Exception {
			return (long) this.annotationCall(annotation, "value");
		}
		
		/**
		 * @param object
		 *            Object which is currently being evaluated.
		 * @param fieldName
		 *            Name of a requested field.
		 * @return Current runtime value in defined field.
		 * @throws Exception
		 *             Value couldn't be obtained (field doesn't exist or is not
		 *             accessible).
		 */
		private long getValueOfField(Object object, String fieldName) throws Exception {
			Class<?>    objectClass    = object.getClass();
			Field       requiredField  = objectClass.getDeclaredField(fieldName);

			requiredField.setAccessible(true);
			return ((Number) requiredField.get(object)).longValue();
		}
		
		/**
		 * Returns all conditions attached to a given field.
		 * 
		 * @param parsedField
		 *            Field which is currently being processed.
		 * @return See {@link AnnotationParser#conditions}.
		 */
		public static Hashtable<Condition, Object> getConditionsOf(Field parsedField) {
			Hashtable<Condition, Object> conditions = new Hashtable<AnnotationParser.Condition, Object>();
			
			for (Condition conditionType : Condition.values()) {
				Class<? extends Annotation> annotationType  = conditionType.getAnnotationType();
				Annotation                  annotation      = parsedField.getAnnotation(annotationType);
				
				if (annotation != null) {
					conditions.put(conditionType, annotation);
				}
			}
			
			return conditions;
		}
	}
	
	/**
	 * <p>Contains informations about field's type and value. Although some people
	 * may tend to think that type of field can be obtained by calling method
	 * {@link Object#getClass()}, it does not work when auto-boxing takes it action.
	 * Value of field of type <i>int</i> will be returned as an instance of class
	 * <i>Integer</i>. It means that information about original type is lost.</p>
	 * 
	 * <p>The problem described above is not really a problem in most of the cases.
	 * But when it comes to invoking of methods using reflection, things gets
	 * worse as seeking for method with parameter of type <i>Integer</i> does
	 * not find a method with parameter of type <i>int</i>.</p>
	 */
	public class RTFieldBean {
		private Class<?>    fieldType;
		private Object      fieldValue;
		
		public RTFieldBean(Class<?> fieldType, Object fieldValue) {
			this.fieldType  = fieldType;
			this.fieldValue = fieldValue;
		}
		
		public Class<?> getFieldType() {
			return fieldType;
		}
		
		public Object getFieldValue() {
			return fieldValue;
		}
	}
	
	/**
	 * @param parsedObject
	 *            Object which contains all fields which will be lately
	 *            processed.
	 */
	public AnnotationParser(Object parsedObject) {
		this.parsedObject   = parsedObject;
		this.parsedClass    = parsedObject.getClass();
	}
	
	/**
	 * Changes current field. By calling this method, some internal values will
	 * be recalculated and all other calls will produce different results than
	 * before.
	 */
	public void setField(Field parsedField) {
		this.parsedField                   = parsedField;
		this.paramSetIgnored               = this.parsedField.getAnnotation(SetIgnored.class);
		this.paramSetType                  = this.parsedField.getAnnotation(SetType.class);
		this.paramSetArray                 = this.parsedField.getAnnotation(SetArray.class);
		this.paramSetCondition             = this.parsedField.getAnnotation(SetCondition.class);
		this.paramSetInjection             = this.parsedField.getAnnotation(SetInjection.class);
		this.paramSetPreVisitListener      = this.parsedField.getAnnotation(SetPreVisitListener.class);
		this.paramSetPostVisitListener     = this.parsedField.getAnnotation(SetPostVisitListener.class);
		this.paramSetItemPostVisitListener = this.parsedField.getAnnotation(SetItemPostVisitListener.class);
		this.conditions                    = Condition.getConditionsOf(this.parsedField);
	}
	
	/**
	 * Checks if field is marked to be processed by this library and evaluates
	 * all declared runtime conditions.
	 * 
	 * @return True if current field should be processed.
	 */
	public boolean isToSet() {
		if (paramSetType != null && paramSetIgnored == null) {
			if (paramSetCondition != null) {
				return evaluateComplexCondition();
			}
			
			Condition condition = this.getCondition();

			if (condition != null) {
				return condition.evaluate(this.conditions, this.parsedObject, this.parsedField);
			} else {
				return true;
			}
		}
		
		return false;
	}

	/**
	 * @return True if field represents a single value (not array).
	 */
	public boolean isValue() {
		return (paramSetType != null && paramSetArray == null);
	}
	
	/**
	 * @return True if field represents an array.
	 */
	public boolean isArray() {
		return (paramSetType != null && paramSetArray != null);
	}
	
	/**
	 * @return True if field should be loaded as a primitive type (not object).
	 */
	public boolean isPrimitiveValue() {
		return (paramSetType != null && paramSetType.type() != BinaryType.OBJECT);
	}

	/**
	 * @return True if field should be loaded as a object (not primitive type).
	 */
	public boolean isObject() {
		return (paramSetType != null && paramSetType.type() == BinaryType.OBJECT);
	}
	
	/**
	 * @return True if field should be loaded using custom converter (for static length).
	 */
	public boolean isCustomStaticType() {
		return (
			paramSetType                    != null &&
			paramSetType.type()             == BinaryType.OBJECT &&
			paramSetType.staticLengthType() != DefaultClass.class
		);
	}

	/**
	 * @return True if field should be loaded using custom converter (for dynamic length).
	 */
	public boolean isCustomDynamicType() {
		return (
			paramSetType                     != null &&
			paramSetType.type()              == BinaryType.OBJECT &&
			paramSetType.dynamicLengthType() != DefaultClass.class
		);
	}
	
	/**
	 * @return Custom converter for type of static length (requested by field).
	 */
	public Class<? extends StaticLengthType> getStaticType() {
		return paramSetType.staticLengthType();
	}
	
	/**
	 * @return Custom converter for type of dynamic length (requested by field).
	 */
	public Class<? extends DynamicLengthType> getDynamicType() {
		return paramSetType.dynamicLengthType();
	}
	
	/**
	 * @return Array of values (with information about original unboxed type) to
	 *         be injected into the constructor of created instance. If there
	 *         are no values to be injected, this array is empty.
	 */
	public RTFieldBean[] getInjectedValues() {
		try {
			RTFieldBean[] injectedValues = new RTFieldBean[(paramSetInjection != null) ? paramSetInjection.fields().length : 0];

			if (injectedValues.length == 0) {
				return injectedValues;
			}
			
			for (int i = 0; i < injectedValues.length; i++) {
				Field field = parsedClass.getDeclaredField(paramSetInjection.fields()[i]);
				
				field.setAccessible(true);
				injectedValues[i] = new RTFieldBean(field.getType(), field.get(parsedObject));
			}
			
			return injectedValues;
		} catch (IllegalAccessException | SecurityException e) {
			throw new RuntimeException("Injection error: Access policy violation.", e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: Invalid argument has been passed while obtaining values to be injected.", e);
		} catch (NoSuchFieldException e) {
			throw new RuntimeException("Injection error: Field to be injected does not exist.", e);
		}
	}
	
	/**
	 * Correctly recognizes if array has static (constant) or dynamic length and
	 * returns its value.
	 * 
	 * @return Length of array.
	 */
	public int getArrayLength() {
		if (paramSetArray.dynamicLength().length() == 0) {
			return paramSetArray.staticLength();
		} else {
			try {
				Field requiredField = this.parsedClass.getDeclaredField(paramSetArray.dynamicLength());
				requiredField.setAccessible(true);
				
				Object value = requiredField.get(parsedObject);
				return ((Number) value).intValue();
			} catch (NoSuchFieldException e) {
				throw new RuntimeException(
					"Length of array is proclaimed to be found in the field which does not exists.",
					e
				);
			} catch (SecurityException | IllegalAccessException e) {
				throw new RuntimeException(
					"Length of array is proclaimed to be found in the field which is not accessible.",
					e
				);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(
					"JAWB internal error: Application has passed an invalid argument while trying to read a field with length of array.",
					e
				);
			}
		}
	}
	
	/**
	 * @return Condition attached to a current field or null.
	 */
	private Condition getCondition() {
		for (Condition condition : conditions.keySet()) {
			if (conditions.get(condition) != null) {
				return condition;
			}
		}
		
		return null;
	}
	
	/**
	 * @return Type of the binary data requested by a field.
	 */
	protected BinaryType getPrimitiveType() {
		return this.paramSetType.type();
	}
	
	/**
	 * Calls method requested by {@link SetCondition}. It must be non-parametric
	 * method returning boolean.
	 * 
	 * @return True if condition should be evaluated.
	 */
	private boolean evaluateComplexCondition() {
		try {
			return (Boolean) callMethod(paramSetCondition.methodName());
		} catch (IllegalAccessException | SecurityException e) {
			throw new RuntimeException("Conditional method could not be called due to access policy.", e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException("Required conditional method does not exist.", e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Application has passed an invalid argument while trying to call a conditional method", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException("Conditional method has thrown an exception.", e);
		}
	}
	
	/**
	 * Calls a non-parametric method with defined name using reflection.
	 * Re-throws possible original exception without any changes.
	 * 
	 * @param methodName
	 *            Name of method to be called.
	 * @return Value returned by invoked method.
	 */
	private Object callMethod(String methodName) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		Method method = parsedClass.getDeclaredMethod(methodName);
		
		method.setAccessible(true);
		return method.invoke(parsedObject);
	}
	
	/**
	 * If {@link SetPreVisitListener} is defined, calls method requested in
	 * {@link SetPreVisitListener#methodName()} or, if empty, calls method
	 * <i>onPreVisitName</i>, where "Name" is a name of currently processed
	 * field.
	 */
	protected void callPreVisitListener() {
		try {
			if (paramSetPreVisitListener != null) {
				if (!this.paramSetPreVisitListener.methodName().isEmpty()) {
					callMethod(paramSetPreVisitListener.methodName());
				} else {
					String fieldName = this.parsedField.getName();
					callMethod("onPreVisit" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1));
				}
			}
		} catch (NoSuchMethodException e) {
			throw new RuntimeException("PreVisitListener: Requested method could not be found.", e);
		} catch (SecurityException | IllegalAccessException e) {
			throw new RuntimeException("PreVisitListener: Requested method could not be called due to access policy.", e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to invoke PreVisitListener.", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException("PreVisitListener: Invoked method has thrown an exception.", e);
		}
	}

	/**
	 * If {@link SetPostVisitListener} is defined, calls method requested in
	 * {@link SetPostVisitListener#methodName()} or, if empty, calls method
	 * <i>onPostVisitName</i>, where "Name" is a name of currently processed
	 * field.
	 */
	protected void callPostVisitListener() {
		try {
			if (paramSetPostVisitListener != null) {
				if (!this.paramSetPostVisitListener.methodName().isEmpty()) {
					callMethod(paramSetPostVisitListener.methodName());
				} else {
					String fieldName = this.parsedField.getName();
					callMethod("onPostVisit" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1));
				}
			}
		} catch (NoSuchMethodException e) {
			throw new RuntimeException("PostVisitListener: Requested method could not be found.", e);
		} catch (SecurityException | IllegalAccessException e) {
			throw new RuntimeException("PostVisitListener: Requested method could not be called due to access policy.", e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to invoke PostVisitListener.", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException("PostVisitListener: Invoked method has thrown an exception.", e);
		}
	}
	
	/**
	 * <p>If {@link SetItemPostVisitListener} is defined, calls method requested in
	 * {@link SetItemPostVisitListener#methodName()} or, if empty, calls method
	 * <i>onItemPostVisitName</i>, where "Name" is a name of currently processed
	 * field.</p>
	 * 
	 * <p>Such a callback listener method must have the two following parameters:
	 *   <ul>
	 *     <li><b>int</b> (index): Index which just has been visited.</li>
	 *     <li><b>ListenerTarget</b> (currentOperation): Type of operation (input or
	 *     output) which is currently being processed.</li>
	 *   </ul>
	 * </p>
	 * <p>User defined callback listener method also have to return value of type "int"
	 * which rewrites a current value of last index in array. It means that if current
	 * value was 0 and this method returned value 1, the next processed item is 2 and
	 * item with index number 1 was skipped.</p>
	 * 
	 * @param index
	 *            Index of item in array which just has been processed.
	 * @param currentOperation
	 *            Type of operation which is currently being processed.
	 * @return Rewrites last index to this value.
	 */
	protected int callItemPostVisitListener(int index, ListenerTarget currentOperation) {
		try {
			if (paramSetItemPostVisitListener != null) {
				String methodName = "";
				
				if (!this.paramSetItemPostVisitListener.methodName().isEmpty()) {
					methodName = paramSetItemPostVisitListener.methodName();
				} else {
					String fieldName = this.parsedField.getName();
					methodName = "onItemPostVisit" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
				}

				Method method = parsedClass.getDeclaredMethod(methodName, int.class, ListenerTarget.class);
				
				method.setAccessible(true);
				return (Integer) method.invoke(parsedObject, index, currentOperation);
			}
			
			return index;
		} catch (NoSuchMethodException e) {
			throw new RuntimeException("ItemPostVisitListener: Requested method could not be found.", e);
		} catch (SecurityException | IllegalAccessException e) {
			throw new RuntimeException("ItemPostVisitListener: Requested method could not be called due to access policy.", e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to invoke ItemPostVisitListener.", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException("ItemPostVisitListener: Invoked method has thrown an exception.", e);
		}
	}
	
	/**
	 * Calls {@link #callItemPostVisitListener(int, ListenerTarget)}.
	 */
	protected int callItemPostSetListener(int index, ListenerTarget currentOperation) {
		if (paramSetItemPostVisitListener != null) {
			ListenerTarget target = paramSetItemPostVisitListener.target();
			
			if (target == ListenerTarget.ALL || target == ListenerTarget.INPUT) {
				return callItemPostVisitListener(index, currentOperation);
			}
		}
		
		return index;
	}

	/**
	 * Calls {@link #callItemPostVisitListener(int, ListenerTarget)}.
	 */
	protected int callItemPostReadListener(int index, ListenerTarget currentOperation) {
		if (paramSetItemPostVisitListener != null) {
			ListenerTarget target = paramSetItemPostVisitListener.target();
			
			if (target == ListenerTarget.ALL || target == ListenerTarget.OUTPUT) {
				return callItemPostVisitListener(index, currentOperation);
			}
		}
		
		return index;
	}
}
